/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package eu.xibit.jadardb;

import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 *
 * @author David
 */
public final class InMemoryFile {
	
	private final class ExtendFileJob implements Runnable {
		private final long newSize;

		public ExtendFileJob(long newSize) {
			this.newSize = newSize;
		}

		@Override
		public void run() {
			try {
				raFile.setLength(newSize);
			} catch (IOException ex) {
				throw new RuntimeException(ex);
			}
		}
	}
	private final class TruncateFileJob implements Runnable {
		private final long newSize;

		public TruncateFileJob(long newSize) {
			this.newSize = newSize;
		}

		@Override
		public void run() {
			try {
				raFile.setLength(newSize);
			} catch (IOException ex) {
				throw new RuntimeException(ex);
			}
		}
	}
	
	private final class FileWriteJob implements Runnable {
		
		private final long fileOffset;
		private final byte[] data;
		private final int offset;
		private final int length;

		public FileWriteJob(long fileOffset, byte[] data, int offset, int length) {
			this.fileOffset = fileOffset;
			this.data = data;
			this.offset = offset;
			this.length = length;
		}

		public long getFileOffset() {
			return fileOffset;
		}

		public byte[] getData() {
			return data;
		}

		public int getOffset() {
			return offset;
		}

		public int getLength() {
			return length;
		}

		@Override
		public void run() {
			try {
				raFile.seek(fileOffset);
				raFile.write(data, offset, length);
			} catch (IOException ex) {
				throw new RuntimeException(ex);
			} finally {
				decrementWaitListSize();
			}
		}
		
	}
	
	private static final int SEGMENT_SIZE = 65536;
	
	
	private final File file;
	private final RandomAccessFile raFile;
	private final ArrayList<byte[]> bytesList = new ArrayList<byte[]>();
	private long fileLength = 0;
	private final ExecutorService fileWriteWorker = Executors.newSingleThreadExecutor();
	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	private volatile int waitingListSize = 0;
	private volatile boolean closed = false;

	public InMemoryFile(File file) throws FileNotFoundException, IOException {
		this.file = file;
		this.raFile = new RandomAccessFile(file, "rws");
		loadFile();
	}
	
	

	
	public long length() {
		if (closed) {
			throw new IllegalStateException("This file is closed.");
		}
		return fileLength;
	}
	
	public void setLength(long newLength) {
		if (closed) {
			throw new IllegalStateException("This file is closed.");
		}
		try {
			lock.writeLock().lock();
			if (closed) {
				throw new IllegalStateException("This file is closed.");
			}
			if (newLength == fileLength) {
				// nothing to do here
			} else if (newLength < fileLength) {
				truncateSize(newLength);
			} else {
				extendSizeIfNeeded(newLength);
			}
		} finally {
			lock.writeLock().unlock();
		}
	}

	public synchronized int getWaitingListSize() {
		return waitingListSize;
	}
	
	
	
	public void write(long fileOffset, int data) {
		if (data < 0) {
			data = 0;
		}
		if (data > 255) {
			data = 255;
		}
		write(fileOffset, new byte[] {(byte)data});
	}
	
	public void write(long fileOffset, byte[] data) {
		write(fileOffset, data, 0, data.length);
	}
	
	public void write(long fileOffset, byte[] data, int offset, int length) {
		if (fileOffset < 0) {
			throw new IllegalArgumentException("Invalid fileOffset parameter: " + fileOffset + "");
		}
		if (data.length < 1) {
			return;
		}
		if (length == 0) {
			return;
		}
		if (length < 0) {
			throw new IllegalArgumentException("Invalid length parameter: " + length + "");
		}
		if (offset < 0) {
			throw new IllegalArgumentException("Invalid offset parameter: " + offset + ".");
		}
		if (offset + length > data.length) {
			throw new IllegalArgumentException("Invalid offset and length parameter: (" + offset + "," + length + "). The data parameters length is smaller then offset + length.");
		}
		
		try {
			lock.writeLock().lock();
			if (closed) {
				throw new IllegalStateException("This file is closed.");
			}
			
			// starting the write job
			byte[] dataToWrite = new byte[length];
			System.arraycopy(data, offset, dataToWrite, 0, length);
			writeInFile(fileOffset, dataToWrite, 0, length);
			
			// callculating segment positions
			int segmentIndex = (int) (fileOffset / SEGMENT_SIZE);
			int segmentOffset = (int) (fileOffset % SEGMENT_SIZE);
			extendSizeIfNeeded(fileOffset + length);
			
			// writing in memory
			int written = 0;
			while (written < length) {
				if (length - written <= SEGMENT_SIZE - segmentOffset) {
					// lastSegment
					System.arraycopy(data, offset + written, bytesList.get(segmentIndex), segmentOffset, length - written);
					written += length - written;
				} else {
					System.arraycopy(data, offset + written, bytesList.get(segmentIndex), segmentOffset, SEGMENT_SIZE - segmentOffset);
					written += SEGMENT_SIZE - segmentOffset;
					segmentIndex++;
					segmentOffset = 0;
				}
			}
		} finally {
			lock.writeLock().unlock();
		}
	}
	
	public void writeInt(long fileOffset, int i) {
		try {
			lock.writeLock().lock();
			if (closed) {
				throw new IllegalStateException("This file is closed.");
			}
			byte[] data = new byte[4];
			data[0] = (byte) ((i >>> 24) & 0xff);
			data[1] = (byte) ((i >>> 16) & 0xff);
			data[2] = (byte) ((i >>> 8) & 0xff);
			data[3] = (byte) ((i) & 0xff);
			write(fileOffset, data);
		} finally {
			lock.writeLock().unlock();
		}
	}
	
	public void writeLong(long fileOffset, long l) {
		try {
			lock.writeLock().lock();
			if (closed) {
				throw new IllegalStateException("This file is closed.");
			}
			byte[] data = new byte[8];
			data[0] = (byte) ((l >>> 56) & 0xff);
			data[1] = (byte) ((l >>> 48) & 0xff);
			data[2] = (byte) ((l >>> 40) & 0xff);
			data[3] = (byte) ((l >>> 32) & 0xff);
			data[4] = (byte) ((l >>> 24) & 0xff);
			data[5] = (byte) ((l >>> 16) & 0xff);
			data[6] = (byte) ((l >>> 8) & 0xff);
			data[7] = (byte) ((l) & 0xff);
			write(fileOffset, data);
		} finally {
			lock.writeLock().unlock();
		}
	}
	
	public void writeUTF(long fileOffset, String s) {
		try {
			lock.writeLock().lock();
			if (closed) {
				throw new IllegalStateException("This file is closed.");
			}
			if (s == null) {
				write(fileOffset, new byte[] {-1, -1});
				return;
			}
			try {
				byte[] data = s.getBytes("UTF-8");
				int len = data.length;
				if (len > 65534) {
					throw new IllegalArgumentException("Cant serialize a string which has more than 65534 bytes.");
				}
				int first = (len >>> 8) & 0xff;
				int second = len & 0xff;
				byte[] fullData = new byte[data.length + 2];
				fullData[0] = (byte) first;
				fullData[1] = (byte) second;
				System.arraycopy(data, 0, fullData, 2, data.length);
				write(fileOffset, fullData);
			} catch (UnsupportedEncodingException ex) {
				// can't happen
				throw new RuntimeException(ex);
			}
		} finally {
			lock.writeLock().unlock();
		}
	}
	
	
	
	public int read(long fileOffset) {
		byte[] res = read(fileOffset, 1);
		if (res != null && res.length == 1) {
			return res[0] & 0xff;
		} else {
			return -1;
		}
	}
	
	public byte[] read(long fileOffset, int size) {
		if (closed) {
			throw new IllegalStateException("This file is closed.");
		}
		byte[] data = new byte[size];
		int readed = read(fileOffset, data);
		if (readed == size) {
			return data;
		} else if (readed == 0) {
			return new byte[0];
		} else if (readed < 0) {
			return null;
		} else {
			byte[] res = new byte[readed];
			System.arraycopy(data, 0, res, 0, readed);
			return res;
		}
	}
	
	public int read(long fileOffset, byte[] data) {
		return read(fileOffset, data, 0, data.length);
	}
	
	public int read(long fileOffset, byte[] data, int offset, int length) {
		if (closed) {
			throw new IllegalStateException("This file is closed.");
		}
		if (fileOffset >= fileLength) {
			return -1;
		}
		if (fileOffset < 0) {
			throw new IllegalArgumentException("The fileOffset must be greater or equals than 0.");
		}
		if (length < 1) {
			return 0;
		}
		if (data.length < offset + length) {
			throw new ArrayIndexOutOfBoundsException("The length of the data array must be greater or equals than the offset + length.");
		}
		
		try {
			lock.readLock().lock();
			int realLen = (int)Math.min(fileLength - fileOffset, (long)length);
			int segmentIndex = (int) (fileOffset / SEGMENT_SIZE);
			int segmentOffset = (int) (fileOffset % SEGMENT_SIZE);
			
			int readed = 0;
			while (readed < realLen) {
				if (realLen - readed <= SEGMENT_SIZE - segmentOffset) {
					// last segment
					System.arraycopy(bytesList.get(segmentIndex), segmentOffset, data, offset + readed, realLen - readed);
					readed += realLen - readed;
				} else {
					// inner segment
					System.arraycopy(bytesList.get(segmentIndex), segmentOffset, data, offset + readed, SEGMENT_SIZE - segmentOffset);
					readed += SEGMENT_SIZE - segmentOffset;
					segmentIndex++;
					segmentOffset = 0;
				}
			}
			return realLen;
		} finally {
			lock.readLock().unlock();
		}
	}
	
	public int readInt(long fileOffset) throws EOFException {
		try {
			lock.readLock().lock();
			byte[] data = read(fileOffset, 4);
			if (data ==null || data.length != 4) {
				throw new EOFException("End of the file reached.");
			}

			int res = 0;
			res |= (data[0] & 0xff) << 24;
			res |= (data[1] & 0xff) << 16;
			res |= (data[2] & 0xff) << 8;
			res |= (data[3] & 0xff);

			return res;
		} finally {
			lock.readLock().unlock();
		}
	}
	
	public long readLong(long fileOffset) throws EOFException {
		try {
			lock.readLock().lock();
			byte[] data = read(fileOffset, 8);
			if (data ==null || data.length != 8) {
				throw new EOFException("End of the file reached.");
			}

			long res = 0;
			res |= (data[0] & 0xffl) << 56;
			res |= (data[1] & 0xffl) << 48;
			res |= (data[2] & 0xffl) << 40;
			res |= (data[3] & 0xffl) << 30;
			res |= (data[4] & 0xffl) << 24;
			res |= (data[5] & 0xff) << 16;
			res |= (data[6] & 0xff) << 8;
			res |= (data[7] & 0xff);

			return res;
		} finally {
			lock.readLock().unlock();
		}
	}
	
	public String readUTF(long fileOffset) throws EOFException {
		try {
			lock.readLock().lock();
			int first = read(fileOffset);
			if (first < 0) {
				throw new EOFException("End of the file reached.");
			}

			int second = read(fileOffset + 1);
			if (second < 0) {
				throw new EOFException("End of the file reached.");
			}
			if (first == 255 && second == 255) {
				return null;
			}
			int len = first << 8 + second;

			byte[] data = read(fileOffset + 2, len);
			if (data == null || data.length < len) {
				throw new EOFException("End of the file reached.");
			}
			try {
				return new String(data, "UTF-8");
			} catch (UnsupportedEncodingException ex) {
				// can't happen
				throw new RuntimeException(ex);
			}
		} finally {
			lock.readLock().unlock();
		}
	}
	
	
	
	private void loadFile() throws IOException {
		long fileSize = raFile.length();
		if (fileSize < 1) {
			return;
		}
		fileLength = fileSize;
		
		long readed = 0;
		while (readed < fileSize) {
			byte[] buff = new byte[SEGMENT_SIZE];
			int inReaded = 0;
			while (inReaded < SEGMENT_SIZE && readed < fileSize) {
				int actReaded = raFile.read(buff, inReaded, SEGMENT_SIZE - inReaded);
				inReaded += actReaded;
				readed += actReaded;
			} 
			bytesList.add(buff);
		}
	}
	
	private synchronized void incrementWaitListSize() {
		waitingListSize++;
	}
	
	private synchronized void decrementWaitListSize() {
		waitingListSize--;
	}
	
	private void writeInFile(long fileOffset, byte[] data, int offset, int length) {
		incrementWaitListSize();
		fileWriteWorker.execute(new FileWriteJob(fileOffset, data, offset, length));
	}
	
	private void extendSizeIfNeeded(long size) {
		if (size < 0) {
			throw new IllegalArgumentException("Illegal size: " + size);
		}
		if (fileLength >= size) {
			// no extension is needed
			return;
		}
		
		fileWriteWorker.execute(new ExtendFileJob(size));
		int neededSegmentCount = (int) (size / SEGMENT_SIZE) + 1;
		while (bytesList.size() < neededSegmentCount) {
			bytesList.add(new byte[SEGMENT_SIZE]);
		}
		fileLength = size;
	}
	
	private void truncateSize(long size) {
		if (size < 0) {
			throw new IllegalArgumentException("Illegal size: " + size);
		}
		
		if (size >= fileLength) {
			return;
		}
		
		fileWriteWorker.execute(new TruncateFileJob(size));
		int neededSegmentCount = (int) (size / SEGMENT_SIZE) + 1;
		while (bytesList.size() > neededSegmentCount) {
			bytesList.remove(bytesList.size() - 1);
		}
		fileLength = size;
	}
	
	public void close() throws IOException {
		if (closed) {
			throw new IllegalStateException("This file is closed.");
		}
		try {
			lock.writeLock().lock();
			closed = true;
			fileWriteWorker.shutdown();
			while (true) {
				try {
					if (fileWriteWorker.awaitTermination(10, TimeUnit.SECONDS)) {
						break;
					}
				} catch (InterruptedException ex) {
				}
			}
			raFile.close();
			bytesList.clear();
		} finally {
			lock.writeLock().unlock();
		}
	}
	
}
